import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

import java.util.*;

public class JE2048 extends JFrame {
    private Container c = getContentPane();
    private int n = 4;
    private int m = 4;
    private int pane[][] = new int[n][m];
    private JButton gui[][] = new JButton[n][m];
    private int curScore = 0;
    private JButton scoreShower = new JButton("分数：0");
    private Random randomInstance = new Random(System.currentTimeMillis());
    private boolean won = false;
    private HashMap<Integer, Color> colormap = new HashMap<>();
    private HashMap<Rule, Integer> rulemap = new HashMap<>();
    private JMenuBar menuBar = new JMenuBar();
    private JPanel centerPane = new JPanel();
    private JLabel northWest = new JLabel("2048 - Java Edition");
    private boolean autoplay = false;
    private ArrayList<Integer> moveableDirections = new ArrayList<>();
    private HashMap<Integer, String> textmap = new HashMap<>();
    private ArrayList<int[][]> history = new ArrayList<>();
    private int historyCursor = 0;
    private boolean scoreBecameInf = false;
    public class JE2048KeyListener extends KeyAdapter {
        @Override
        public void keyPressed(KeyEvent e) {
            if (autoplay) return;
            int c = e.getKeyCode();
            if (!autoplay) {
                if (c == KeyEvent.VK_UP || c == KeyEvent.VK_W) moveAtDirection(0);
                else if (c == KeyEvent.VK_DOWN || c == KeyEvent.VK_S) moveAtDirection(2);
                else if (c == KeyEvent.VK_LEFT || c == KeyEvent.VK_A) moveAtDirection(3);
                else if (c == KeyEvent.VK_RIGHT || c == KeyEvent.VK_D) moveAtDirection(1);
                refreshGUI();
            }
            if (judgeLose()) {
                JOptionPane.showMessageDialog(null, "你输了", "好似", JOptionPane.ERROR_MESSAGE);
                reInitBoard();
            }
            if (judgeWin()) {
                Object[] options = {"继续", "重开"};
                int n = JOptionPane.showOptionDialog(null, "你赢了", "开摆", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, null, options, options);
                if (n == 1) {
                    reInitBoard();
                }
            }
        }
    }
    private int[] pseudoMove(int a[]) {
        int n = a.length;
        // move
        for (int j = 1; j < n; j++) {
            int k = j;
            while (k != 0 && a[k - 1] == 0) {
                a[k - 1] = a[k];
                a[k] = 0;
                k--;
            }
        }
        // merge
        for (int j = 0; j < n - 1; j++) {
            Rule craftRule = new Rule(a[j], a[j + 1]);
            if (rulemap.containsKey(craftRule)) {
                int result = rulemap.get(craftRule);
                a[j] = result;
                curScore += a[j];
                a[j + 1] = 0;
            }
        }
        // move again
        for (int j = 1; j < n; j++) {
            int k = j;
            while (k != 0 && a[k - 1] == 0) {
                a[k - 1] = a[k];
                a[k] = 0;
                k--;
            }
        }
        return a;
    }
    private int get4InXXXOutOfOne(int i) {
        int rn1 = randomInstance.nextInt(i);
        if (rn1 != i - 1) rn1 = 2;
        else rn1 = 4;
        return rn1;
    }
    public boolean judgeLose() {
        moveableDirections = new ArrayList<Integer>();
        // try to move up
        boolean lose = true;
        int prevScore = curScore;
        int[][] origPane = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        moveUp();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) lose = false;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                pane[i][j] = origPane[i][j];
            }
        }
        curScore = prevScore;
        if (!lose) moveableDirections.add(0);
        // try to move down
        lose = true;
        prevScore = curScore;
        origPane = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        moveDown();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) lose = false;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                pane[i][j] = origPane[i][j];
            }
        }
        curScore = prevScore;
        if (!lose) moveableDirections.add(2);
        // try to move left
        lose = true;
        prevScore = curScore;
        origPane = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        moveLeft();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) lose = false;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                pane[i][j] = origPane[i][j];
            }
        }
        curScore = prevScore;
        if (!lose) moveableDirections.add(3);
        // try to move right
        lose = true;
        prevScore = curScore;
        origPane = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        moveRight();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) lose = false;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                pane[i][j] = origPane[i][j];
            }
        }
        curScore = prevScore;
        if (!lose) moveableDirections.add(1);
        Collections.sort(moveableDirections);
        return moveableDirections.size() == 0;
    }
    public boolean judgeWin() {
        if (won && !autoplay) return false;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (pane[i][j] == 2048) {
                    won = true;
                    return true;
                }
            }
        }
        return false;
    }
    private void moveUp() {
        if (n == 1) return;
        int origPane[][] = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        for (int i = 0; i < m; i++) {
            int moveBefore[] = new int[n];
            for (int j = 0; j < n; j++) moveBefore[j] = pane[j][i];
            moveBefore = pseudoMove(moveBefore);
            for (int j = 0; j < n; j++) pane[j][i] = moveBefore[j];
        }
        boolean changed = false;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) changed = true;
            }
        }
        if (changed) {
            int ri1 = randomInstance.nextInt(n), rj1 = randomInstance.nextInt(m);
            while (pane[ri1][rj1] != 0) {
                ri1 = randomInstance.nextInt(n);
                rj1 = randomInstance.nextInt(m);
            }
            int rn1 = get4InXXXOutOfOne(10);
            pane[ri1][rj1] = rn1;
        }
    }
    private void moveDown() {
        if (n == 1) return;
        int origPane[][] = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        for (int i = 0; i < m; i++) {
            int moveBefore[] = new int[n];
            for (int j = 0; j < n; j++) moveBefore[j] = pane[n - 1 - j][i];
            moveBefore = pseudoMove(moveBefore);
            for (int j = 0; j < n; j++) pane[n - 1 - j][i] = moveBefore[j];
        }
        boolean changed = false;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) changed = true;
            }
        }
        if (changed) {
            int ri1 = randomInstance.nextInt(n), rj1 = randomInstance.nextInt(m);
            while (pane[ri1][rj1] != 0) {
                ri1 = randomInstance.nextInt(n);
                rj1 = randomInstance.nextInt(m);
            }
            int rn1 = get4InXXXOutOfOne(10);
            pane[ri1][rj1] = rn1;
        }
    }
    private void moveLeft() {
        if (m == 1) return;
        int origPane[][] = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        for (int i = 0; i < n; i++) {
            int moveBefore[] = new int[m];
            for (int j = 0; j < m; j++) moveBefore[j] = pane[i][j];
            moveBefore = pseudoMove(moveBefore);
            for (int j = 0; j < m; j++) pane[i][j] = moveBefore[j];
        }
        boolean changed = false;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) changed = true;
            }
        }
        if (changed) {
            int ri1 = randomInstance.nextInt(n), rj1 = randomInstance.nextInt(m);
            while (pane[ri1][rj1] != 0) {
                ri1 = randomInstance.nextInt(n);
                rj1 = randomInstance.nextInt(m);
            }
            int rn1 = get4InXXXOutOfOne(10);
            pane[ri1][rj1] = rn1;
        }
    }
    private void moveRight() {
        if (m == 1) return;
        int origPane[][] = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        for (int i = 0; i < n; i++) {
            int moveBefore[] = new int[m];
            for (int j = 0; j < m; j++) moveBefore[j] = pane[i][m - 1 - j];
            moveBefore = pseudoMove(moveBefore);
            for (int j = 0; j < m; j++) pane[i][m - 1 - j] = moveBefore[j];
        }
        boolean changed = false;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) changed = true;
            }
        }
        if (changed) {
            int ri1 = randomInstance.nextInt(n), rj1 = randomInstance.nextInt(m);
            while (pane[ri1][rj1] != 0) {
                ri1 = randomInstance.nextInt(n);
                rj1 = randomInstance.nextInt(m);
            }
            int rn1 = get4InXXXOutOfOne(10);
            pane[ri1][rj1] = rn1;
        }
    }
    public void reInitBoard() {
        pane = new int[n][m];
        gui = new JButton[n][m];
        won = false;
        autoplay = false;
        scoreBecameInf = false;
        history.clear();
        northWest.setText("2048 - Java Edition");
        pack();
        c.remove(centerPane);
        centerPane = new JPanel();
        centerPane.setLayout(new GridLayout(n, m));
        int ri1 = randomInstance.nextInt(n), rj1 = randomInstance.nextInt(m), ri2 = randomInstance.nextInt(n), rj2 = randomInstance.nextInt(m);
        while (ri1 == ri2 && rj1 == rj2) {
            ri2 = randomInstance.nextInt(n);
            rj2 = randomInstance.nextInt(m);
        }
        int rn1 = get4InXXXOutOfOne(10);
        int rn2 = get4InXXXOutOfOne(10);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                pane[i][j] = 0;
                gui[i][j] = new JButton("");
                gui[i][j].setEnabled(false);
                gui[i][j].addKeyListener(new JE2048KeyListener());
                if (i == ri1 && j == rj1) {
                    pane[i][j] = rn1;
                }
                if (i == ri2 && j == rj2) {
                    pane[i][j] = rn2;
                }
                gui[i][j].setFont(new Font("Consolas", Font.PLAIN, 24));
                centerPane.add(gui[i][j]);
            }
        }
        c.add(centerPane, BorderLayout.CENTER);
        centerPane.addKeyListener(new JE2048KeyListener());
        curScore = 0;
        refreshGUI();
        updateHistory();
    }
    public void addRule(int a, int b, int result) {
        rulemap.put(new Rule(a, b), result);
    }
    public void removeRule(int a, int b) {
        Rule r = new Rule(a, b);
        if (rulemap.containsKey(r)) {
            rulemap.remove(r);
        }
    }
    public HashMap<Rule, Integer> getRules() {
        return rulemap;
    }
    private Color getColor(int num) {
        Color lack = new Color(132, 132, 132);
        Color res = colormap.getOrDefault(num, lack);
        if (!res.equals(lack)) {
            return res;
        }
        int power = num;
        power--;
        power |= power >> 1;
        power |= power >> 2;
        power |= power >> 4;
        power |= power >> 8;
        power |= power >> 16;
        power++;
        int lessPower = power / 2;
        Color colorLess = colormap.getOrDefault(lessPower, lack);
        Color colorGreat = colormap.getOrDefault(power, lack);
        if (colorLess.equals(lack) || colorGreat.equals(lack)) {
            colormap.put(num, lack);
            return lack;
        }
        int lessRed = colorLess.getRed();
        int highRed = colorGreat.getRed();
        double blendedRed = lessRed * 1.0 * (power - num) / (power - lessPower) + highRed * 1.0 * (num - lessPower) / (power - lessPower);
        int lessGreen = colorLess.getGreen();
        int highGreen = colorGreat.getGreen();
        double blendedGreen = lessGreen * 1.0 * (power - num) / (power - lessPower) + highGreen * 1.0 * (num - lessPower) / (power - lessPower);
        int lessBlue = colorLess.getBlue();
        int highBlue = colorGreat.getBlue();
        double blendedBlue = lessBlue * 1.0 * (power - num) / (power - lessPower) + highBlue * 1.0 * (num - lessPower) / (power - lessPower);
        int br = (int) blendedRed;
        br %= 255;
        int bg = (int) blendedGreen;
        bg %= 255;
        int bb = (int) blendedBlue;
        bb %= 255;
        Color c = new Color(br, bg, bb);
        colormap.put(num, c);
        return c;
    }
    public void refreshGUI() {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                gui[i][j].setText(textmap.getOrDefault(pane[i][j], ""));
                gui[i][j].setBackground(getColor(pane[i][j]));
            }
        }
        refreshRule();
        if (!scoreBecameInf && curScore >= 0) scoreShower.setText("分数：" + curScore);
        else {
            scoreBecameInf = true;
            scoreShower.setText("分数：∞");
        }
        pack(); c.repaint(); c.revalidate();
    }
    private void initialize() {
        colormap.put(0, new Color(205, 193, 180));
        colormap.put(2, new Color(238, 228, 218));
        colormap.put(4, new Color(237, 224, 200));
        colormap.put(8, new Color(242, 177, 121));
        colormap.put(16, new Color(245, 149, 99));
        colormap.put(32, new Color(246, 124, 95));
        colormap.put(64, new Color(246, 94, 59));
        colormap.put(128, new Color(237, 207, 114));
        colormap.put(256, new Color(237, 207, 97));
        colormap.put(512, new Color(237, 207, 80));
        colormap.put(1024, new Color(237, 207, 63));
        colormap.put(2048, new Color(237, 207, 46));
        colormap.put(4096, new Color(255, 181, 0));
        colormap.put(8192, new Color(255, 182, 0));
        colormap.put(16384, new Color(255, 184, 0));
        colormap.put(32768, new Color(250, 181, 0));
        colormap.put(65536, new Color(255, 126, 41));
        colormap.put(131072, new Color(255, 109, 40));
        colormap.put(262144, new Color(255, 91, 40));
        colormap.put(524288, new Color(255, 192, 49));
        colormap.put(1048576, new Color(255, 191, 20));
        colormap.put(2097152, new Color(254, 194, 0));
        colormap.put(4194304, new Color(221, 176, 0));
        colormap.put(8388608, new Color(218, 73, 31));
        colormap.put(16777216, new Color(244, 123, 82));
        colormap.put(33554432, new Color(255, 168, 136));
        colormap.put(67108864, new Color(255, 206, 185));
        colormap.put(134217728, new Color(164, 68, 0));
        colormap.put(268435456, new Color(169, 74, 0));
        colormap.put(536870912, new Color(174, 92, 21));
        colormap.put(1073741824, new Color(179, 114, 51));
        colormap.put(-2147483648, new Color(255, 255, 255));
        for (int i = 1; i < 31; i++) {
            textmap.put(1 << i, "" + (1 << i));
        }
        textmap.put(1 << 31, "∞");
        northWest.setFont(new Font("Arial", Font.PLAIN, 36));
        JPanel north = new JPanel();
        north.setLayout(new BorderLayout());
        north.add(northWest, BorderLayout.WEST);
        JPanel northEast = new JPanel();
        scoreShower.setBackground(Color.LIGHT_GRAY);
        scoreShower.setForeground(Color.BLACK);
        scoreShower.setFont(new Font("微软雅黑", Font.PLAIN, 18));
        northEast.add(scoreShower);
        north.add(northEast, BorderLayout.EAST);
        c.add(north, BorderLayout.NORTH);
        setBoardSize(4, 4);
        reInitBoard();
        scoreShower.addKeyListener(new JE2048KeyListener());
        addKeyListener(new JE2048KeyListener());
        c.addKeyListener(new JE2048KeyListener());
    }
    private void initMenuBar() {
        JMenu menu = new JMenu("设置");
        JMenuItem highlevelSetting = new JMenuItem("高级设置...");
        menu.add(highlevelSetting);
        highlevelSetting.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                new DLC(JE2048.this);
            }
        });
        menuBar.add(menu);
    }
    public JE2048() {
        super("2048 (Java Edition)");
        setSize(600, 600);
        setVisible(true);
        setDefaultCloseOperation(3);
        setResizable(false);
        setJMenuBar(menuBar);
        initialize();
        initMenuBar();
    }
    public void refreshRule() {
        for (int i = 1; i <= 30; i++) {
            if (!rulemap.containsKey(new Rule(1 << i, 1 << i))) addRule(1 << i, 1 << i, 2 << i);
        }
        addRule(1 << 31, 1 << 31, 1 << 31);
    }
    public void setBoardSize(int n, int m) {
        this.n = n;
        this.m = m;
        refreshRule();
    }
    public int getBoardWidth() {
        return n;
    }
    public int getBoardHeight() {
        return m;
    }
    public int[][] getBoard() {
        return pane;
    }
    public void setBoardAtWithoutRefresh(int i, int j, int elem) {
        pane[i][j] = elem;
    }
    public void setBoardAt(int i, int j, int elem) {
        setBoardAtWithoutRefresh(i, j, elem);
        refreshGUI();
    }
    public boolean moveAtDirectionWithoutRefresh(int dir) {
        int[][] origPane = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                origPane[i][j] = pane[i][j];
            }
        }
        if (dir == 0) moveUp();
        else if (dir == 1) moveRight();
        else if (dir == 2) moveDown();
        else if (dir == 3) moveLeft();
        updateHistory();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (origPane[i][j] != pane[i][j]) {
                    return true;
                }
            }
        }
        return false;
    }
    public boolean moveAtDirection(int dir) {
        boolean result = moveAtDirectionWithoutRefresh(dir);
        refreshGUI();
        return result;
    }
    public void setAutoplaying() {
        this.autoplay = true;
        northWest.setText("2048 - Java Edition [Autoplay]");
        refreshGUI();
    }
    public boolean isAutoPlaying() {
        return autoplay;
    }
    public int getScore() {
        return curScore;
    }
    public void setScore(int score) {
        curScore = score;
    }
    public ArrayList<Integer> getMoveableDirections() {
        return moveableDirections;
    }
    public HashMap<Integer, String> getTextMapping() {
        return textmap;
    }
    public HashMap<Integer, Color> getColorMapping() {
        return colormap;
    }
    public void updateHistory() {
        if (autoplay) return;
        if (historyCursor != history.size() - 1) {
            while (history.size() - 1 > historyCursor) {
                history.remove(history.size() - 1);
            }
        }
        int[][] pane = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                pane[i][j] = this.pane[i][j];
            }
        }
        history.add(pane);
        historyCursor = history.size() - 1;
    }
    public void undo() {
        historyCursor--;
        if (historyCursor < 0) {
            historyCursor = 0;
            return;
        }
        int[][] pane = history.get(historyCursor);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                this.pane[i][j] = pane[i][j];
            }
        }
        refreshGUI();
    }
    public void redo() {
        historyCursor++;
        if (historyCursor >= history.size()) {
            historyCursor = history.size() - 1;
            return;
        }
        int[][] pane = history.get(historyCursor);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                this.pane[i][j] = pane[i][j];
            }
        }
        refreshGUI();
    }
    public static void main(String[] args) {
        new JE2048();
    }
}